// Unity C# reference source
// Copyright (c) Unity Technologies. For terms of use, see
// https://unity3d.com/legal/licenses/Unity_Reference_Only_License

using System;
using System.Linq;
using System.Text;
using UnityEngine;

namespace UnityEditor.PackageManager.UI.Internal
{
    [Serializable]
    internal class AssetStoreListOperation : IOperation
    {
        private const int k_QueryLimit = 500;
        private static readonly string k_UserNotLoggedInErrorMessage = L10n.Tr("User not logged in.");

        public string packageUniqueId => string.Empty;
        public long productId => 0;

        [SerializeField]
        protected long m_Timestamp;
        public long timestamp => m_Timestamp;

        public long lastSuccessTimestamp => 0;

        public bool isOfflineMode => false;

        [SerializeField]
        protected bool m_IsInProgress;
        public virtual bool isInProgress => m_IsInProgress;

        public bool isProgressVisible => false;

        public bool isInPause => false;

        public RefreshOptions refreshOptions => RefreshOptions.Purchased;

        public bool isProgressTrackable => false;

        public float progressPercentage => 0;

        public event Action<IOperation, UIError> onOperationError = delegate {};
        public virtual event Action<IOperation> onOperationSuccess = delegate {};
        public virtual event Action<IOperation> onOperationFinalized = delegate {};
        public event Action<IOperation> onOperationProgress = delegate {};

        [SerializeField]
        private PurchasesQueryArgs m_OriginalQueryArgs;
        [SerializeField]
        private PurchasesQueryArgs m_AdjustedQueryArgs;

        [SerializeField]
        private AssetStorePurchases m_Result;
        public virtual AssetStorePurchases result => m_Result;

        [NonSerialized]
        private IUnityConnectProxy m_UnityConnect;
        [NonSerialized]
        private IAssetStoreRestAPI m_AssetStoreRestAPI;
        [NonSerialized]
        private IAssetStoreCache m_AssetStoreCache;
        public void ResolveDependencies(IUnityConnectProxy unityConnect, IAssetStoreRestAPI assetStoreRestAPI, IAssetStoreCache assetStoreCache)
        {
            m_UnityConnect = unityConnect;
            m_AssetStoreRestAPI = assetStoreRestAPI;
            m_AssetStoreCache = assetStoreCache;
        }

        private AssetStoreListOperation()
        {
        }

        public AssetStoreListOperation(IUnityConnectProxy unityConnect, IAssetStoreRestAPI assetStoreRestAPI, IAssetStoreCache assetStoreCache)
        {
            ResolveDependencies(unityConnect, assetStoreRestAPI, assetStoreCache);
        }

        public virtual void Start(PurchasesQueryArgs queryArgs)
        {
            SetQueryArgs(queryArgs);
            m_IsInProgress = true;
            m_Timestamp = DateTime.Now.Ticks;

            if (!m_UnityConnect.isUserLoggedIn)
            {
                OnOperationError(new UIError(UIErrorCode.AssetStoreOperationError, k_UserNotLoggedInErrorMessage));
                return;
            }

            m_Result = new AssetStorePurchases(m_OriginalQueryArgs);
            if (m_OriginalQueryArgs.status is PageFilters.Status.Downloaded or PageFilters.Status.Imported or PageFilters.Status.UpdateAvailable && !m_AdjustedQueryArgs.productIds.Any())
            {
                m_Result.total = 0;
                onOperationSuccess?.Invoke(this);
                FinalizedOperation();
                return;
            }

            // We need to keep a local version of the current timestamp to make sure the callback timestamp is still the original one.
            var localTimestamp = m_Timestamp;
            m_AssetStoreRestAPI.GetPurchases(m_AdjustedQueryArgs, purchases => GetPurchasesCallback(purchases, localTimestamp), OnOperationError);
        }

        public virtual void Stop()
        {
            m_Timestamp = DateTime.Now.Ticks;
            m_AssetStoreRestAPI.AbortGetPurchases(m_AdjustedQueryArgs);
            FinalizedOperation();
        }

        private void SetQueryArgs(PurchasesQueryArgs queryArgs)
        {
            m_OriginalQueryArgs = queryArgs;

            // The GetPurchases API has a limit of maximum 1000 items (to avoid performance issues)
            // therefore we do some adjustments to the original query args enforce that limit and split
            // the original query to multiple batches. We make a clone before when adjusting is needed
            m_AdjustedQueryArgs = m_OriginalQueryArgs.Clone();
            m_AdjustedQueryArgs.limit = Math.Min(m_OriginalQueryArgs.limit, k_QueryLimit);

            switch (m_OriginalQueryArgs.status)
            {
                case PageFilters.Status.Downloaded:
                    m_AdjustedQueryArgs.status = PageFilters.Status.None;
                    m_AdjustedQueryArgs.productIds = m_AssetStoreCache.localInfos.Select(info => info.productId).ToList();
                    break;
                case PageFilters.Status.Imported:
                    m_AdjustedQueryArgs.status = PageFilters.Status.None;
                    m_AdjustedQueryArgs.productIds = m_AssetStoreCache.importedPackages.Select(p => p.productId).ToList();
                    break;
                case PageFilters.Status.UpdateAvailable:
                    m_AdjustedQueryArgs.status = PageFilters.Status.None;
                    var productIdsToCheck = m_AssetStoreCache.localInfos.Select(i => i.productId)
                        .Concat(m_AssetStoreCache.importedPackages.Select(i => i.productId)).ToHashSet();
                    m_AdjustedQueryArgs.productIds = productIdsToCheck.Where(id =>
                    {
                        var updateInfo = m_AssetStoreCache.GetUpdateInfo(id);
                        if (updateInfo == null)
                            return false;

                        var localInfo = m_AssetStoreCache.GetLocalInfo(id);
                        if (localInfo != null && localInfo.uploadId != updateInfo.recommendedUploadId)
                            return true;

                        var importedPackage = m_AssetStoreCache.GetImportedPackage(id);
                        return importedPackage != null && importedPackage.uploadId != updateInfo.recommendedUploadId;
                    }).ToList();
                    break;
            }
        }

        private void GetPurchasesCallback(AssetStorePurchases purchases, long operationTimestamp)
        {
            if (operationTimestamp != m_Timestamp)
                return;

            if (!m_UnityConnect.isUserLoggedIn)
            {
                OnOperationError(new UIError(UIErrorCode.AssetStoreOperationError, k_UserNotLoggedInErrorMessage));
                return;
            }

            m_Result.AppendPurchases(purchases);

            if (m_OriginalQueryArgs.limit > k_QueryLimit && m_IsInProgress)
            {
                var numAssetsToFetch = (int)m_Result.total - m_OriginalQueryArgs.startIndex;
                var numAlreadyFetched = m_Result.list.Count;
                var newLimit = Math.Min(k_QueryLimit, Math.Min(numAssetsToFetch, m_OriginalQueryArgs.limit) - numAlreadyFetched);
                if (newLimit > 0)
                {
                    m_AdjustedQueryArgs.startIndex = m_OriginalQueryArgs.startIndex + m_Result.list.Count;
                    m_AdjustedQueryArgs.limit = newLimit;
                    m_AssetStoreRestAPI.GetPurchases(m_AdjustedQueryArgs, purchases => GetPurchasesCallback(purchases, operationTimestamp), OnOperationError);
                    return;
                }
            }

            onOperationSuccess?.Invoke(this);
            FinalizedOperation();
        }

        private void OnOperationError(UIError error)
        {
            onOperationError?.Invoke(this, error);
            FinalizedOperation();

            PackageManagerOperationErrorAnalytics.SendEvent(GetType().Name, error);
        }

        private void FinalizedOperation()
        {
            m_IsInProgress = false;
            onOperationFinalized?.Invoke(this);

            onOperationError = delegate {};
            onOperationFinalized = delegate {};
            onOperationSuccess = delegate {};
            onOperationProgress = delegate {};
        }
    }
}
